'use client' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'next/navigation' import { useApiCallback, useApiFetch } from '@saaslib/nextjs' import { loadStripe } from '@stripe/stripe-js' import { useAdminConfig } from '../../../../lib/use-admin-config' import { Badge } from '../../../../components/ui/badge' import { Button } from '../../../../components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card' import { Input } from '../../../../components/ui/input' import { Label } from '../../../../components/ui/label' import { Select } from '../../../../components/ui/select' type AdminUser = { id: string email: string name?: string role?: string blocked?: boolean emailVerified?: boolean stripeCustomerId?: string subscriptions?: Record< string, { product: string periodEnd?: string nextProduct?: string cancelled?: boolean stripeSubscriptionId?: string } > } type AdminUserResponse = { user: AdminUser } type SubscriptionCatalogResponse = { subscriptions: Array<{ type: string; products: Array<{ id: string; prices: string[] }> }> } export default function UserDetailPage() { const params = useParams() const rawUserId = typeof params?.id === 'string' ? params.id : Array.isArray(params?.id) ? params.id[0] : '' const userId = rawUserId === 'undefined' ? '' : rawUserId const { config } = useAdminConfig() const { data, loading, error, refetch } = useApiFetch(`/admin/users/${userId}`, { skip: !userId, skipDefault: null, }) const { data: catalogData } = useApiFetch('/admin/subscriptions/catalog') const { callback: updateUser, loading: saving } = useApiCallback() const { callback: changeSubscription, loading: changingPlan } = useApiCallback<{ ok: true }>() const { callback: startSubscription, loading: starting } = useApiCallback<{ sessionId: string }>() const { callback: grantSubscription, loading: granting } = useApiCallback<{ ok: true }>() const { callback: cancelSubscription, loading: cancelling } = useApiCallback<{ ok: true }>() const { callback: resumeSubscription, loading: resuming } = useApiCallback<{ ok: true }>() const user = data?.user const [formState, setFormState] = useState({ name: '', email: '', role: '', blocked: false, }) const subscriptions = useMemo(() => user?.subscriptions ?? {}, [user]) const catalogTypes = useMemo(() => catalogData?.subscriptions ?? [], [catalogData]) const allTypes = useMemo(() => { const fromCatalog = catalogTypes.map((item) => item.type) const fromUser = Object.keys(subscriptions) return Array.from(new Set([...fromCatalog, ...fromUser])) }, [catalogTypes, subscriptions]) useEffect(() => { if (user) { setFormState({ name: user.name ?? '', email: user.email, role: user.role ?? '', blocked: !!user.blocked, }) } }, [user]) const handleSave = async () => { const payload = { name: formState.name || undefined, email: formState.email || undefined, role: formState.role || undefined, blocked: formState.blocked, } await updateUser(`/admin/users/${userId}`, { method: 'PATCH', body: JSON.stringify(payload), }) refetch() } const handlePlanChange = async (type: string, priceId: string) => { if (!priceId) return await changeSubscription('/admin/subscriptions/change', { method: 'POST', body: JSON.stringify({ userId, type, priceId }), }) refetch() } const handleStart = async (type: string, priceId: string) => { if (!priceId) return const res = await startSubscription('/admin/subscriptions/start', { method: 'POST', body: JSON.stringify({ userId, type, priceId }), }) if (res?.sessionId) { const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY if (!stripeKey) { alert('Stripe publishable key is missing. Set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY to start subscriptions.') return } const stripe = await loadStripe(stripeKey) if (!stripe) { alert('Failed to initialize Stripe.') return } await stripe.redirectToCheckout({ sessionId: res.sessionId }) } } const handleGrant = async (type: string, productId: string) => { if (!productId) return await grantSubscription('/admin/subscriptions/grant', { method: 'POST', body: JSON.stringify({ userId, type, productId }), }) refetch() } const handleCancel = async (type: string) => { if (!confirm('Cancel this subscription at period end?')) return await cancelSubscription('/admin/subscriptions/cancel', { method: 'POST', body: JSON.stringify({ userId, type }), }) refetch() } const handleResume = async (type: string) => { await resumeSubscription('/admin/subscriptions/resume', { method: 'POST', body: JSON.stringify({ userId, type }), }) refetch() } if (!userId) { return (
Invalid user id.
) } return (

User detail

Manage profile, access, and billing.

{loading &&
Loading user…
} {error && (
{error.message}
)} {user && ( <> Profile
setFormState((prev) => ({ ...prev, name: event.target.value }))} />
setFormState((prev) => ({ ...prev, email: event.target.value }))} />
ID: {user.id} {user.emailVerified ? ( Email verified ) : ( Email unverified )}

Subscriptions

Upgrade, cancel, or resume plans without leaving the console.

{Object.keys(subscriptions).length === 0 && (
No subscriptions found for this user.
)} {allTypes.map((type) => { const subscription = subscriptions?.[type] const catalog = catalogData?.subscriptions?.find((item) => item.type === type) const configCatalog = config.subscriptions.find((item) => item.type === type) return ( {configCatalog?.label ?? type} {subscription ? (

Current product: {subscription.product} {subscription.nextProduct ? ` · Next: ${subscription.nextProduct}` : ''}

) : (

No active subscription for this type.

)} {subscription ? (
) : (
)} {!subscription && (

This writes the product directly to the user record without Stripe.

)} {subscription?.cancelled && Cancel pending}
) })}
)}
) }